在 Elixir 中,要稱呼一個具名函式,有個固定的格式:模組.函式名/參數個數
。這個名稱格式廣泛用於文件及網路上的溝通。打開 iex 試試看:
iex(0)> h Enum.map/2
# 會印出該函式的文件...
若你定義了接收不同參數個數的同名函式,或是宣告了有預設式的函式,都會產生兩種不同參數個數的檔案名稱,例如內建的集合處理函式,就有 Enum.reduce/3
及 Enum.reduce/2
兩種,分別是需要傳入起始值,及只接收兩個參數,直接用集合第一個元素當起始值的。
&
捕獲運算子 (capture operator)再來看看 JavaScript ,具名函式可以直接傳遞。所以遇到在 lambda 中將接收到的參數原封不動傳給具名函式,並回傳其結果的情況下,可以直接傳遞具名函式。這就是之前提到的 λ-calculus 裡的一條規則,叫做 eta reduction (η-reduction)。講起來很拗口,看範例就很直覺:
/* JavaScript */
[-1, -2, 3].map(i => Math.abs(i))
// η-reduction
[-1, -2, 3].map(Math.abs)
但在 Elixir 中,具名函式不加括號視同零參數的呼叫,因此我們需要有辦法將具名函式轉換成 lambda。這就是 &
的第一個用法。在轉換其它 module 的函式時,語法就是 &
加上剛剛提到的函式正式名稱: &Module.function/arity
。
Enum.map([:a, :b, :c], fn a -> Atom.to_string(a) end)
# η-reduction
Enum.map([:a, :b, :c], &Atom.to_string/1)
當然轉換模組內的 function 或是用 import 語法匯入的函式也沒問題,不加 Module 名稱就可以。
def double(i), do: i * 2
Enum.map([1, 2, 3], &double/1)
#=> [2, 4, 6]
順帶一提,用 &
捕獲/生成的函式不一定要寫在高階函式中,也可以另外指派給變數。由於被轉換成匿名函式了,所以呼叫時要用 .()
。
f = &Kernel.is_atom/1
f.(:atom) #=> true
昨天提到了匿名函式的宣告是 fn -> end
,其實相當冗長。因此遇到函式本體很短的情況下會覺得麻煩。
Enum.map([1, 2, 3], fn i -> i * 2 end)
這種情況就是 &
運算子派上用場的另一個地方,順帶一提,也是最多人感到困惑的用法。&1
是函式接收到的參數。
Enum.map([1, 2, 3], &(&1 * 2))
#=> [2, 4, 6]
換句話說,fn i -> i * 2 end
跟 &(&1 * 2)
是一樣的意思。
既然有 &1
, 可以推測出要多個參數也是可以的:
fn = &(&1 + &2 + &3)
fn.(1, 2, 3) #=> 6
若用 [] 或 {} 代替圓括號,呼叫後的結果會分別是 List 及 Tuple
l = &[&1, &2]
l.(1, 2)
#=> [1, 2]
t = &{&1, &2}
t.(1, 2)
#=> {1, 2}
&()
的使用判準雖然短的很方便,濫用 &()
語法的話,程式很容易就會變得難讀。我個人的判準是內部超過 10 個字元,或是有三個以上的運算子,就寧願用 fn -> end
來宣告了。
那麼要做出 Haskell 的 identity 就簡單了: &(&1)
。
Enum.group_by(["a", "b", "c", "a", "b"], &(&1))
#=> %{"a" => ["a", "a"], "b" => ["b", "b"], "c" => ["c"]}
想一下,如果有個函式需要三個參數,你只傳一個參數給它,會發生什麼事?寫 JavaScript 的人會說「其它的參數會是 undefined
」。其它 OO 語言的人會說「會噴錯誤」。但真的只能這樣嗎?
在 functional programming 裡,有一個概念叫 partial application。就是當你呼叫函式,但只傳進部份的參數時,他會綁定你傳入的參數,但回傳另一個需要剩下的參數的函式。因為 arguments are partially applied,這個行為的名詞化就叫 partial application。雖然有人翻成部份應用,但我覺得這個詞翻譯了反而更難懂。在 elixir 裡,就是透過 &
來做這件事:
take_five = &Enum.take(&1, 5)
take_five.(1..100)
# => [1, 2, 3, 4, 5]
與 partial application 常常一起提到的另一個詞叫 curry,柯里化。跟那種棕色的好吃食物沒有關係,這個字是說把接收 n 個參數的函式,轉換成 n 層只接收一個參數的函式。
在 Elixir 裡,沒有內建 curry 函式,而且因為 &
不支援嵌套,而且呼叫匿名函式要用 .()
,所以寫起來不太漂亮:
add_all = fn a -> fn b -> fn c -> a + b + c end end end
add_all.(1).(2).(3)
#=> 6
之前提到的 JavaScript FP 函式庫 Ramda 就有提供 curry 函式幫你把一般函式柯里化。而在 ES6 裡,也可以用箭頭函式來宣告:
/* JavaScript */
add_all = a => b => c => a + b + c
add_all(1)(2)(3)
#=> 6
Module.function/arity
來指稱一個函式h/1
可以印出函式說明&
運算子可以把具名函式轉成 lambda 來傳遞&
也可以用來寫出很短的函式&
還可以拿來做 partial application
函式宣告的部份就到此為止了。明天將開始介紹各種資料型別了。
Happy hacking! 明天見。